iT邦幫忙

2024 iThome 鐵人賽

DAY 5
1
JavaScript

TypeScript 初學者也能看的學習指南系列 第 5

TypeScript 初學者也能看的學習指南 05 - Object 物件

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20240915/201493626RpYDbfOcX.png

本篇要介紹的是 TypeScript 型別系統中的 object。object 是屬於 Object Types 的一部分
你將會了解到 object 在 TypeScript 中的寫法、特性以及和 JavaScript object 的不同之處


官方文件上寫了這段話

In JavaScript, the fundamental way that we group and pass around data is through objects. In TypeScript, we represent those through object types.

在 JavaScript 中我們透過物件 object去組織和傳遞資料,但在 TypeScript 中,則是透過 物件型別 object types

嘿嘿~這張圖又出現了
Object Types(物件型別)是個統稱,object, tuple, array, enum, function 都包含在內,這裡先談談原始的 object
https://ithelp.ithome.com.tw/upload/images/20240914/20149362AfHOQjogHl.png


object

在 TypeScript 中,物件 object 的「型別註釋」可以使用以下格式來宣告,一般是不會用 : object
因為 function, array, tuple, enum 也是屬於 object type 的一部分,直接定義 : object 無法一眼看出是什麼型別

: {
  <key>: <value type>;
}

屬性 (key) 的分隔可以使用 分號(;)逗號(,),甚至省略不寫都OK。分號或省略不寫在 TypeScript 中更為常見(interface 和 type aalias 也是)

來看看底下準備的幾個情境範例:

範例 1: 符合物件定義的格式、型別

product 裡有 name, price, category, inStock 這幾個屬性
賦值都有遵守定義的格式和型別,所以這段 ✅ Pass

// 型別註釋
let product: {
  name: string;
  price: number;
  category: string;
  inStock: boolean;
}

// 賦值
product = {
  name: 'Apple',
  price: 10,
  category: 'Fruit',
  inStock: true
}

範例 2: 更改物件中的值

當更改物件中的值時,完全沒有問題!當然前提是要符合最初定義的型別

product.price = 100; // ✅ Pass
product.price = '100'; // ❌ Type 'string' is not assignable to type 'number'.
product.inStock = false

範例 3:「不」符合物件定義的格式、型別

賦值時故意少寫instock,結果是 ❌ 報錯啦~
就算在下面補上 instock 也是一樣

// 賦值 
product = {
  name: 'Apple',
  price: 10,
  category: 'Fruit',
}
// ❌ Property 'inStock' is missing ...

product.inStock = false;
product['inStock'] = false;

範例 4: 新增外來屬性(noImplicitAny 為 true)

想要在 product 裡新增 id 屬性,結果會如下:

product.id = 'v231323';
// ❌ Property 'id' does not exist on type '{ name: string; price: number; category: string; inStock: boolean; }'.

product['id'] = 'v231323';
// ❌ TS7053: Element implicitly has an 'any' type because expression of type '"id"' can't be used to index type '{ name: string; price: number; category: string; inStock: boolean; }'.
// Property 'id' does not exist on type '{ name: string; price: number; category: string; inStock: boolean; }'.

不論透過 .[] 新增,我們都無法在 product 裡加上 id
原因是在型別推斷上,TypeScript Compiler 並沒找到 id,代表這個物件被加了預期外的東西,在 TypeScript 中是不允許的

範例 5: 新增外來屬性(noImplicitAny 為 false)

product.id = 'v231323';
// ❌ Property 'id' does not exist on type '{ name: string; price: number; category: string; inStock: boolean; }'.

product['id'] = 'v231323'; // ✅ Pass

這裡居然通過了😯
為什麼呢?

  • . 新增屬性
    當用 product.id = 'v231323' 來新增屬性時,TypeScript Compiler 會檢查這個屬性是否已在型別定義中出現。如果沒有,就會報錯。這是為了保證型別的安全性和一致性

  • [] 新增屬性
    當用 product['id'] = 'v231323' 來新增屬性時,TypeScript Compiler 會認為這是一個動態的存取方式。雖然 id 不在型別定義裡,但這種存取方式允許新增屬性,不會觸發編譯錯誤
    不過前提是 noImplicitAny 也要關閉就是了~

❗️雖然會通過,但非常非常非常不建議這樣做,因為它破壞了 TypeScript 靜態型別檢查的目的


為什麼不能像 JavaScript 一樣任意新增屬性?

https://ithelp.ithome.com.tw/upload/images/20240915/20149362MXSu3BF7T6.png
在 JavaScript 中,我們可以動態的向物件新增屬性,因為

  • JavaScript動態型別語言,物件的結構可隨時變動
  • TypeScript靜態型別語言,它的目的是在編譯時,提前捕捉型別錯誤
    所以,當我們定義一個物件,TypeScript 會假設這個物件結構是固定的,不該有額外的屬性被新增,避免在後續使用這個物件時,因結構不一致而導致錯誤

如果我們想要對物件新增屬性,該怎麼做呢?

這裡繼續沿用 product 裡加上 id 的例子

  1. 直接在物件裡新增 id 屬性
let product: {
  id: string;
  name: string;
}
  1. 索引簽名(Index Signatures)
    索引簽名是 TypeScript 提供的功能,讓我們可以使用 索引(即鍵) 來「動態訪問」和「設置物件的屬性」,這在處理各種動態和不確定的資料結構(如來自 API 的 JSON )時特別有用

{ [key: string]: string } 即索引簽名,意思是

  • product 是一個物件
  • [key: string] 表示 key 不限定個數、名稱,但型別得是字串
  • : string 表示 value 只能是字串
// 1.
let product: { [key: string]: string };
product = {
  name: 'Apple',
};

// 2.
let product: {
  name: string;
  price: number;
  category: string;
  inStock: boolean;
  [key: string]: any; // 允許動態屬性,實戰上不建議使用 any,後續章節會再介紹
};

product = {
  name: 'Apple',
  price: 10,
  category: 'Fruit',
  inStock: true
};

product.id = 'v231323'; // ✅ Pass
  1. Record 型別
    Record<Keys, Type> 是屬於 TypeScript utility types 的一種,它可以用來表示任意鍵值對的物件
    utility types 後面章節會介紹
let product: Record<string, any> = {
  name: 'Apple',
  price: 10,
};

product.id = 'v231323'; // ✅ Pass
  1. 可選屬性 ?
    如果你無法保證一定都會有 id 屬性(以此範例來說,實戰上一般是會有),可以使用可選屬性 來註記非必要的屬性
let product: {
  name: string;
  price: number;
  category: string;
  inStock: boolean;
  id?: string; // 可選屬性
};

product = {
  name: 'Apple',
  price: 10,
  category: 'Fruit',
  inStock: true
};

product.id = 'v231323'; // ✅ Pass

P.S. 可選屬性 (?) !== 可選串連 (?.)


補充

物件字面量(Object Literal, 花括號 {} ) 是在 JavaScript 和 TypeScript 中直接定義物件的語法
在 JavaScript 中物件字面量是開放式的(Object literals are open-ended),我們可以任意對物件屬性新增、修改

var obj = { a: 1 };
obj.b = 2; // ✅ Pass 新增屬性 

如果想要在 JavaScript 中加以限制型別,可以使用 JSDoc
JSDoc 是 用在 JavaScript 中的一種標記語言,語法以 以 /** 開始,以 */ 結束
透過對函式、變數、回傳值、參數等的註釋,加以限制型別以提高維護性和安全性,還可以掃描程式碼並自動生成文檔,不過這裡就不深入談

/** 
* @type {{a: number}}  // obj 只包含 a 這個屬性,型別為 number
*/     

var obj = { a: 1 }; 
obj.b = 2;  // ❌ Property 'b' does not exist on type '{ a: number; }'.

總結

  1. 定義物件的型別、屬性時,賦值的格式都要符合原本最初的定義,缺一不可
  2. noImplicitAny 為 false 時,物件可透過 [] 動態新增屬性,但非常不建議這樣做
  3. 動態新增物件屬性除了直接宣告以外,還可透過 Record 型別索引簽名可選屬性 做到

每天講的內容有推到 github 上喔


References


上一篇
TypeScript 初學者也能看的學習指南 04 - 型別系統: 型別註釋 & 型別推斷
下一篇
TypeScript 初學者也能看的學習指南 06 - Array 陣列
系列文
TypeScript 初學者也能看的學習指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言